</> 技術筆記Tech Notes

有限狀態機在 JavaScript 之應用

有限狀態機在 JavaScript 之應用

長期以來,有限狀態機(Finite State Machine, FSM)被用作在事件驅動程式(如網路介面卡和編譯器)中設計和實現複雜行為的組織原理。如今,可程式化的 Web 瀏覽器為新一代的應用程式開闢了一個全新的事件驅動環境。由 Ajax 推廣的瀏覽器應用程式正變得越來越複雜,其設計師和開發者可以從有限狀態機提供的紀律和結構中獲益。

在本文中,您將學習如何使用有限狀態機為一個簡單的 Web 小工具——一個可以動畫進出畫面的工具提示(Tooltip)——設計其複雜的行為。

從基本到更精細的工具提示

多數現代圖形化應用程式,當滑鼠停留在按鈕、輸入框等視覺控制項上時,都能顯示一個包含說明或建議的小文字框,我們通常稱之為「工具提示」(Tooltip) 。

像 Chrome、Firefox 和 Edge 等主流瀏覽器,會為任何具有 title 屬性的 HTML 元素顯示內建的工具提示。然而,這種內建工具提示的行為與外觀是寫死在瀏覽器中的,無法更改。它們的出現和消失都非常突然,且內容僅限於純文字。

為了提升使用者體驗,我們可以打造一個功能更豐富的工具提示。一個設計精良的工具提示應具備以下特點:

  • 動畫效果:逐漸淡入和淡出,而不是突然出現和消失。

  • 豐富內容:可以包含圖片、格式化文字和自訂樣式。

  • 動態追蹤:當滑鼠在元素上移動時,工具提示會跟隨滑鼠。

  • 智慧反應:當滑鼠短暫移開又移回時,動畫可以反轉,提供更流暢的互動。

這種增強的行為提高了可用性。人類的視覺系統對運動特別敏感,一個會移動和動畫的工具提示更容易被使用者注意到。而要實現這種複雜的、依賴於事件順序的行為,有限狀態機便是最理想的設計模式。

什麼是有限狀態機?

有限狀態機是一種數學計算模型,它將複雜的行為分解為一系列獨立的狀態(States)、觸發狀態改變的事件(Events),以及從一個狀態轉移到另一個狀態的轉換(Transitions)

一個有限狀態機包含:

  • 狀態 (States):程式在等待事件時所處的各種情況。例如,「閒置中」、「等待中」、「淡入中」。

  • 事件 (Events):觸發狀態改變的外部或內部訊號。例如,「滑鼠移入」、「計時器到期」。

  • 轉換 (Transitions):根據當前狀態和發生的事件,決定下一個要進入的狀態。

  • 動作 (Actions):在進行狀態轉換時執行的具體操作。例如,「啟動計時器」、「更新畫面」。

FSM 最常見的兩種表示法是狀態圖和二維狀態表。兩者皆能清晰地表達邏輯,本文將使用狀態圖來進行初步設計。

使用狀態圖設計 FadingTooltip

我們的 FadingTooltip 小工具行為,可以用一個狀態圖來清晰地描述。首先,我們定義它所處的幾個核心狀態:

  • 未啟動 (Inactive):初始狀態,等待滑鼠進入目標區域。

  • 暫停 (Pause):滑鼠已經進入,正在等待使用者停留片刻。

  • 淡入 (FadeIn):確認使用者停留,工具提示開始逐漸顯示。

  • 顯示 (Display):工具提示完全可見,並會跟隨滑鼠移動。

  • 淡出 (FadeOut):滑鼠移開或顯示時間結束,工具提示開始逐漸消失。

接著,我們定義觸發狀態轉變的事件與對應的動作。下方的狀態圖草稿 展示了完整的邏輯流程。

stateDiagram-v2
    [*] --> 未啟動

    未啟動 --> 暫停: 滑鼠懸停 / 記錄滑鼠位置<br/>開始計時

    暫停 --> 未啟動: 滑鼠離開 / 清除計時器
    暫停 --> 暫停: 滑鼠移動 / 記錄滑鼠位置<br/>重設計時器
    暫停 --> 淡入: 時間到 / 在滑鼠位置建立工具提示<br/>啟動淡入計時器

    淡入 --> 顯示: 計時器(tick) / 遞增不透明度<br/>當不透明度到達最大值<br/>→ 停止計時器, 啟動顯示計時器
    淡入 --> 淡入: 滑鼠移動 / 移動工具提示至游標處
    淡入 --> 淡出: 滑鼠離開

    顯示 --> 淡出: 時間到 或 滑鼠離開
    顯示 --> 顯示: 滑鼠移動 / 移動工具提示至游標處

    淡出 --> 未啟動: 計時器(tick) / 遞減不透明度<br/>當不透明度到達 0<br/>→ 停止計時器, 刪除工具提示
    淡出 --> 淡入: 滑鼠懸停
    淡出 --> 淡出: 滑鼠移動 / 移動工具提示至游標處

狀態詳解

  1. 未啟動 (Inactive State)

    • 此為初始狀態。當 滑鼠懸停 (mouseover) 事件發生時,記錄當前滑鼠位置,啟動一個短暫的計時器,並轉換到 暫停 狀態。

  2. 暫停 (Pause State)

    • 如果計時器還沒到期,滑鼠離開 (mouseout),則取消計時器,返回 未啟動 狀態。

    • 如果 滑鼠移動 (mousemove),則更新滑鼠位置並重設計時器,繼續停在 暫停 狀態。

    • 如果 時間到 (timeout),表示滑鼠停留時間足夠長,此時應建立工具提示的 DOM 元素,啟動動畫計時器,並轉換到 淡入 狀態。

  3. 淡入 (FadeIn State)

    • 在此狀態,計時器 (tick) 事件會週期性觸發,每次都增加工具提示的透明度。

    • 當透明度達到最大值時,停止此計時器,啟動一個用於控制顯示時間的新計時器,並轉換到 顯示 狀態。

    • 如果在淡入過程中 滑鼠離開,則直接轉換到 淡出 狀態,開始反向動畫。

  4. 顯示 (Display State)

    • 工具提示完全可見。如果 滑鼠移動,工具提示會跟隨移動。

    • 時間到滑鼠離開 時,轉換到 淡出 狀態。

  5. 淡出 (FadeOut State)

    • 計時器 (tick) 事件會週期性減少工具提示的透明度。

    • 當透明度降至 0 時,銷毀工具提示的 DOM 元素,並返回到 未啟動 狀態,等待下一次觸發。

    • 如果在淡出過程中,滑鼠又移回了元素上 (滑鼠懸停),則應立刻停止淡出,轉回 淡入 狀態,使工具提示重新顯示。

從圖表到程式碼:實現策略

雖然本文著重於設計,但我們可以簡單探討實現的策略。JavaScript 的語言特性,如閉包和一級函式,非常適合用來實現有限狀態機。

開發者可以定義一個 StateMachine,並透過 TransitionFunction 類別 或一個二維的關聯陣列來編碼狀態表,將「目前狀態」和「觸發事件」映射到對應的「動作」和「下一個狀態」。在比較不同的實作方法時,例如使用 InterfaceAbstract Class ,可以從幾個維度評估其優劣:

  • 實作數量 Interface 更靈活,一個類別可以實作多個介面。

  • 新增/刪除事件的影響 使用狀態表(如關聯陣列)通常比基於類別的方法更容易增刪事件,因為邏輯更集中,修改時的影響範圍較小 。

結論

使用有限狀態機來設計複雜的 UI 行為,可以將原本糾結的 if-else 邏輯,轉化為清晰、可預測的狀態轉換表或狀態圖。這種方法雖然前期需要更縝密的設計,但換來的是更簡單的程式碼、更容易的測試與維護。對於現代 Web 應用中日益複雜的互動需求,FSM 無疑是一個強而有力的開發工具。